package cz.mff.curreco.services;

import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
import java.io.InputStream;
import java.util.ArrayList;

import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.RenderedOp;

import com.sun.media.jai.codec.SeekableStream;
import com.sun.media.jai.util.ImageUtil;

import cz.mff.curreco.model.Circle;
import cz.mff.curreco.model.CircleResultItem;
import cz.mff.curreco.model.OneCircleResult;
import cz.mff.curreco.utils.Convert;
import cz.mff.curreco.utils.ImageUtils;
import cz.mff.curreco.utils.NormalizePicture;

public class ImageService implements IImageService {

	
	int[] input;
	int width;
	int height;
	int[] acc;
	int accSize=1;
	int[] results;
	int r;
	
	/**
	 * Class used for counting black and white pixels
	 * used as argument for processing circles 
	 */
	private class ColorCouter
	{
		//count of white pixels
		int white = 0;
		//count of black pixels
		int black = 0;
	}
	
	@Override
	public int shapeRecognize(InputStream input) {
		// TODO
		
		BufferedImage bitmapImage = Convert.streamToImage(input);
		if (bitmapImage==null)
			return 0;
		
		
		return 1;
	}

	@Override
	public BufferedImage threshold(BufferedImage image, int param) {
		int color;
		System.out.println(image.getWidth());
		System.out.println(image.getHeight());
		for(int i=0;i<image.getWidth();i++) {
			for(int j=0;j<image.getHeight();j++) {
				color = ImageUtils.getRGB(image, i, j);
				if (ImageUtils.getAvg(color)<param) {
					image.setRGB(i, j, ImageUtils.createRGBColor(255, 255, 255));
				} else {
					image.setRGB(i, j, ImageUtils.createRGBColor(0, 0, 0));
				}
				color = ImageUtils.getRGB(image, i, j);
			}
		}
		return image;
	}
	
	@Override
	public BufferedImage edgeDetection(BufferedImage image) {
		return staticEdgeDetection(image);
	}
	
	public static BufferedImage staticEdgeDetection(BufferedImage image) {
		SeekableStream s = SeekableStream.wrapInputStream(Convert.imageToStream(image), true);
		PlanarImage im0 = (PlanarImage)JAI.create("stream", s);
		RenderedOp renderedImage = JAI.create("gradientmagnitude", im0);
		return renderedImage.getRendering().getAsBufferedImage();
	}

	@Override
	public int getRadius(BufferedImage image, double ratio) {
		BufferedImage bi = edgeDetection(image);
		bi = threshold(bi, 110);
		bi = threshold(bi, 110);
		bi = ImageUtils.resizeImage(bi, ratio);
		width = bi.getWidth();
		height = bi.getHeight();
		input = Convert.imageToIntArray(bi);
		int globalMax = 0;
		int minimalRadius = (int)(width*0.25);
		Circle c = new Circle(0, 0, 0);
		r = 0;
		double [] sinCache = new double[360];
		double [] cosCache = new double[360];
		int i;
		for (i=0; i<360; i++) {
			sinCache[i]= Math.sin((i * 3.14159265) / 180);
			cosCache[i]= Math.cos((i * 3.14159265) / 180);
		}
		
		
		for ( i=width/2;i>minimalRadius;i-=2) { 
			r = i;
			acc = new int[width * height + width + 1];		
			int x0, y0;
			double t;
				
			for(int x=0;x<width;x++) {		
				for(int y=0;y<height;y++) {
				
					if ((input[y*width+x] & 0xff)== 255) {
					
						/*for (int theta=0; theta<360; theta++) {
							t = (theta * 3.14159265) / 180;
							x0 = (int)Math.round(x - r * Math.cos(t));
							y0 = (int)Math.round(y - r * Math.sin(t));
							if(x0 < width && x0 > 0 && y0 < height && y0 > 0) {
								acc[x0 + (y0 * width)] += 1;
							}
						}*/
						for (int theta=0; theta<360; theta++)
						{
							x0 = (int)Math.round(x - r *cosCache[theta]);
							y0 = (int)Math.round(y - r *sinCache[theta]);
							if(x0 < width && x0 > 0 && y0 < height && y0 > 0) {
								acc[x0 + (y0 * width)] += 1;
							}
						}
						
					}
				}
			}
			int max=0;
			for(int x=0;x<width;x++) {
				for(int y=0;y<height;y++) {
	
					if (acc[x + (y * width)] > max) {
						max = acc[x + (y * width)];
					}
				}
			}
			if (max>globalMax) {
				globalMax = max;	
				System.out.println("Max :" + max);
				int value;
				for(int x=0;x<width;x++) {
					for(int y=0;y<height;y++) {
						value = (int)(((double)acc[x + (y * width)]/(double)max)*255.0);
						acc[x + (y * width)] = 0xff000000 | (value << 16 | value << 8 | value);
					}
				}
				c = findMaxima();
			}
			if (globalMax>205) {
				break;
			}
		}
		return (int)(c.getDiameter()/ratio);
	}
	

	@Override
	public Circle houghTransformation(BufferedImage image, int radius) {
		
		BufferedImage bi = edgeDetection(image);
		bi = threshold(bi, 110);
		bi = threshold(bi, 110);
		width = bi.getWidth();
		height = bi.getHeight();
		
		System.out.println("FIND CIRCLE WITH RADIUS: "+radius);
		System.out.println("WIDTH: "+width);
		System.out.println("HEIGHT: "+height);
		
		input = Convert.imageToIntArray(bi);
		int globalMax = 0;
		Circle c = new Circle(0, 0, 0);
		r = radius;
		acc = new int[width * height + width + 1];		
		int x0, y0;
		double t;
		double [] sinCache = new double[360];
		double [] cosCache = new double[360];
		int i;
		for (i=0; i<360; i++) {
			sinCache[i]= Math.sin((i * 3.14159265) / 180);
			cosCache[i]= Math.cos((i * 3.14159265) / 180);
		}
		
		for(int x=0;x<width;x++) {			
			for(int y=0;y<height;y++) {
			
				if ((input[y*width+x] & 0xff)== 255) {
					/*
					for (int theta=0; theta<360; theta++) {
						t = (theta * 3.14159265) / 180;
						x0 = (int)Math.round(x - r * Math.cos(t));
						y0 = (int)Math.round(y - r * Math.sin(t));
						if(x0 < width && x0 > 0 && y0 < height && y0 > 0) {
							acc[x0 + (y0 * width)] += 1;
						}
					}*/
					for (i=0;i<360;i++)
					{
						x0 = (int)Math.round(x - r*cosCache[i]);
						y0 = (int)Math.round(y - r*sinCache[i]);
						if(x0 < width && x0 > 0 && y0 < height && y0 > 0) {
							acc[x0 + (y0 * width)] += 1;
						}
					}
				}
			}
		}
		int max=0;
		for(int x=0;x<width;x++) {
			for(int y=0;y<height;y++) {

				if (acc[x + (y * width)] > max) {
					max = acc[x + (y * width)];
				}
			}
		}
		if (max>globalMax) {
			globalMax = max;	
			System.out.println("Max :" + max);
			int value;
			for(int x=0;x<width;x++) {
				for(int y=0;y<height;y++) {
					value = (int)(((double)acc[x + (y * width)]/(double)max)*255.0);
					acc[x + (y * width)] = 0xff000000 | (value << 16 | value << 8 | value);
				}
			}
			c = findMaxima();
		}
		System.out.println("done");
		c.setCenterX((int)(c.getCenterX()));
		c.setCenterY((int)(c.getCenterY()));
		c.setDiameter((int)(c.getDiameter()));
		return c;
	}
	
	
	private Circle findMaxima() {
		results = new int[accSize*3];
		for(int x=0;x<width;x++) {
			for(int y=0;y<height;y++) {
				int value = (acc[x + (y * width)] & 0xff);
				if (value > results[(accSize-1)*3]) {
					results[(accSize-1)*3] = value;
					results[(accSize-1)*3+1] = x;
					results[(accSize-1)*3+2] = y;
					int i = (accSize-2)*3;
					while ((i >= 0) && (results[i+3] > results[i])) {
						for(int j=0; j<3; j++) {
							int temp = results[i+j];
							results[i+j] = results[i+3+j];
							results[i+3+j] = temp;
						}
						i = i - 3;
						if (i < 0) break;
					}
				}
			}
		}
		
		System.out.println("r: " + r + ", x: " + results[1] + ", y: " + results[2]);
		return new Circle(r*2, results[1], results[2]);
	}



	@Override
	public OneCircleResult getBlackPixelsOnCircle(Circle c, BufferedImage input, int halfSize) {
		BufferedImage bi = input;
		ColorCouter cc= new ColorCouter();
		
		int radius = (int)Math.floor((c.getDiameter()/2));
		
		int x = 0;
		int y = 0;
		for (int i= 0;i<radius;i++)
		{
			float q = (float)Math.pow(radius, 2);
			float r = (float)Math.pow(i, 2);
			
			float divX = (float)Math.pow(q - r, 0.5);
			int divY=i;
			
			
			x = Math.round(c.getCenterX() + divX);
			y = c.getCenterY() + divY;
			
			processPoint(bi,x,y,cc,halfSize);
			
			x = Math.round(c.getCenterX() - divX);
			y = c.getCenterY() - divY;
			
			processPoint(bi,x,y,cc,halfSize);

			
			x = Math.round(c.getCenterX() - divX);
			y = c.getCenterY() + divY;
			processPoint(bi,x,y,cc,halfSize);
			
			x = Math.round(c.getCenterX() + divX);
			y = c.getCenterY() - divY;
			processPoint(bi,x,y,cc,halfSize);
			
			
			float temp = divX;
			divX = divY;
			divY = Math.round(temp);
			
			
			x = Math.round(c.getCenterX() + divX);
			y = c.getCenterY() + divY;
			processPoint(bi,x,y,cc,halfSize);
			
			x = Math.round(c.getCenterX() - divX);
			y = c.getCenterY() - divY;
			processPoint(bi,x,y,cc,halfSize);
			
			x = Math.round(c.getCenterX() - divX);
			y = c.getCenterY() + divY;
			processPoint(bi,x,y,cc,halfSize);
			
			x = Math.round(c.getCenterX() + divX);
			y = c.getCenterY() - divY;
			processPoint(bi,x,y,cc,halfSize);
			
			
		}
		float descr = 0;
		int blackCounter = cc.black;
		int whiteCounter = cc.white;
		if(blackCounter != 0)
		{
			descr=whiteCounter/blackCounter;			
		}
		System.out.println("white: " + whiteCounter + " black: " + blackCounter);
		System.out.println("white/black: " + descr);
		OneCircleResult res = new OneCircleResult();
		res.setBlack(blackCounter);
		res.setResult(Convert.imageToStream(bi));
		return res;
	}
	
	
	
	/**
	 * Process point of circle
	 * @param img Source image
	 * @param x coordinate
	 * @param y coordinate
	 * @param cc Counter of white and black pixels
	 */
	private void processPoint(BufferedImage img, int x, int y, ColorCouter cc, int size)
	{
		if(size != 0)
		{			
			for (int i = -size;i<=size;i++)
			{
				for (int j=-(size - Math.abs(i));j<=size - Math.abs(i);j++)
				{					
					int color = img.getRGB(x+i, y+j);
					if(color == ImageUtils.createRGBColor(0, 0, 0))
					{
						cc.black ++;
					}
					else if (color == ImageUtils.createRGBColor(255, 255, 255))
					{
						cc.white++;
					}
					img.setRGB(x+i, y+j, ImageUtils.createRGBColor(255, 0, 0));					
				}			
			}
		}
		else
		{		
			int color = img.getRGB(x, y);
			if(color == ImageUtils.createRGBColor(0, 0, 0))
			{
				cc.black ++;
			}
			else if (color == ImageUtils.createRGBColor(255, 255, 255))
			{
				cc.white++;
			}
			img.setRGB(x,y, ImageUtils.createRGBColor(255, 0, 0));
		}
	}



	@Override
	public CircleResultItem getCircles(Circle c ,int numberOfCircles,InputStream input, int halfSize) 
	{	
		Circle playground = new Circle();
		playground.setCenterX(c.getCenterX());
		playground.setCenterY(c.getCenterY());
		playground.setDiameter(c.getDiameter());
		double x = 0;
		double dia = c.getDiameter();
		double diff = dia/numberOfCircles;
		if(halfSize != 0)
		{
			playground.setDiameter(c.getDiameter()-halfSize);
			x= halfSize;
			dia = playground.getDiameter() - halfSize - 1;
			diff = dia/numberOfCircles;
		}
		//InputStream actStream = input;
		OneCircleResult oneCircle;
		ArrayList<Float> descriptor = new ArrayList<Float>();
		BufferedImage bi = Convert.streamToImage(input);
		
		while (Math.floor(x+ diff*10 + halfSize) > bi.getWidth() || Math.floor(x+ diff*10 + halfSize) > bi.getHeight())
		{
			dia--;
			diff = dia/numberOfCircles;
		}		
			dia = dia -10;
			diff = dia/numberOfCircles;
		
		
		for (int j=0;j<numberOfCircles;j++)
		{			
			x+=diff;
			Circle cc = playground;
			cc.setDiameter((int)Math.floor(x));
			oneCircle = getBlackPixelsOnCircle(cc, bi,halfSize);
			//actStream = oneCircle.getResult();
			descriptor.add(oneCircle.getBlack());			
		}
		
		CircleResultItem res = new CircleResultItem();
		res.setDescriptor(descriptor);
		//res.setResult(actStream);
		//res.setResult(input);
		return res;
	}
	
	@Override
	public BufferedImage normalizeImage(InputStream input)
	{		
		return NormalizePicture.Normalize(Convert.streamToImage(input));
	}
	
	@Override
	public BufferedImage noiseReduction(BufferedImage image) {
		int windowSize = 3;
		int iMax = image.getWidth() / windowSize;
		int jMax = image.getHeight() / windowSize;
		for (int i=0; i<iMax; i++) {
			for (int j=0; j<jMax; j++) {
				int windowColor = getWindowColor(image, i, j, windowSize);
				fillWindow(windowColor ,image, i, j, windowSize);
				
			}
		}
		return image;
	}	
	
	private int getWindowColor(BufferedImage image, int i, int j, int windowSize) {
		int blackCount = 0;
		int whiteCount = 0;
		int pixelCount = windowSize*windowSize;
		int black = ImageUtils.createRGBColor(0, 0, 0);
		int white = ImageUtils.createRGBColor(255, 255, 255);
		for(int x=i*windowSize; x<(i*windowSize)+windowSize; x++) {
			for(int y=j*windowSize; y<(j*windowSize)+windowSize; y++) {
				if (image.getRGB(x, y)==black) {
					blackCount++;
				} else {
					whiteCount++;
				}
			}
			if ((blackCount > pixelCount/2) || (whiteCount > pixelCount/2)) {
				break;
			}
		}
		if (blackCount>whiteCount) {
			return black;
		} else {
			return white;
		}		
	}
	
	private void fillWindow(int color, BufferedImage image, int i, int j, int windowSize) {
		for(int x=i*windowSize; x<(i*windowSize)+windowSize; x++) {
			for(int y=j*windowSize; y<(j*windowSize)+windowSize; y++) {
				image.setRGB(x, y, color);
			}
		}	
	}
}

